#define KEY_HELP (0)
#define KEY_VERSION (1)
#define KEY_TYPE (2)
#define KEY_FILE (3)
#define KEY_MEMORY (4)
#define KEY_TMPDIR (5)
#define KEY_FILELIST (6)
#define KEY_DISKLIST (7)
#include "config.h"

#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wpedantic"

#include <fuse.h>
#include <fuse_opt.h>

#pragma GCC diagnostic pop

#include <libgen.h>
#include <limits.h>
#include <syslog.h>

#include <cassert>
#include <cerrno>

#include "fuse-vm.h"
#include "types.h"
#include "fuseVMData.h"

struct vmfuse_param param;
/**
 * Print usage information
 */
void print_usage()
{
    fprintf(stderr, "usage: %s [options] <file> <mountpoint>\n\n", PROGRAM);
    fprintf(stderr,
            "general options:\n"
            "    -o opt,[opt...]        mount options\n"
            "    -h                     print help\n"
            "    -v                     print version\n"
            "    -f                     don't detach from terminal\n"
            "    -d                     turn on debugging, also implies -f\n"
            "    -m                     use memory to store new disk file data, other type file always use memory\n"
            "    -s opt                 tmp disk file store directory, used to store new disk file data with the same name disk file, not used when -m is specified\n"
            "    -t opt                 target file type (none, file, vsphere, fsphere)\n"
            "    -i opt                 target file path (absolute path or relative path)\n"
            "    -p opt                 backup filelist (split by comma)\n"
            "    -l opt                 backup disklist (split by comma)\n"
            "\n");
    fprintf(stderr, "example:\n"
                    "    ./vmfuse -d -f -o auto_unmount -t file -i /root/tmp/test.txt /mnt/test\n"
                    "    ./vmfuse -d -f -o auto_unmount -t vsphere -i /root/tmp/vsphere.vmdk /mnt/test\n"
                    "    ./vmfuse -d -f -o auto_unmount -t fsphere -i /root/tmp/fsphere.img /mnt/test\n"
                    "    ./vmfuse -d -f -o auto_unmount -t vsphere -p /root/full,/root/incr -l test1.vmdk,test2.vmdk /mnt/test\n"
                    "    ./vmfuse -d -f -o auto_unmount -m -t none /mnt/test\n"
                    "    ./vmfuse -d -f -o auto_unmount -s /root/tmpdir -t none /mnt/test\n");
}

/**
 * Print version information (fuse-zip and FUSE library)
 */
void print_version()
{
    fprintf(stderr, "%s version: %s\n", PROGRAM, VERSION);
}

/**
 * Function to process arguments (called from fuse_opt_parse).
 *
 * @param data  Pointer to vmfuse_param structure
 * @param arg is the whole argument or option
 * @param key determines why the processing function was called
 * @param outargs the current output argument list
 * @return -1 on error, 0 if arg is to be discarded, 1 if arg should be kept
 */
static int process_arg(void *data, const char *arg, int key, struct fuse_args *outargs)
{
    struct vmfuse_param *p_param = static_cast<vmfuse_param *>(data);

    (void)outargs;

    // 'magic' fuse_opt_proc return codes
    // KEEP: 让 fuse_opt_parse() 继续解析 该参数，按 fuse_opt_parse 规则处理
    // DISCARD: process_arg() 已经完全处理 该参数，不让 fuse_opt_parse() 继续解析
    // ERROR: 解析失败，终止 fuse_opt_parse()
    const static int KEEP = 1;
    const static int DISCARD = 0;
    const static int ERROR = -1;

    switch (key)
    {
        case KEY_HELP:
        {
            print_usage();
            p_param->help = true;
            return DISCARD;
        }
        case KEY_VERSION:
        {
            print_version();
            p_param->version = true;
            return KEEP;
        }
        case KEY_TYPE:
        {
            char tmp_type[16]= {0};
            sscanf(arg, "-t%s", tmp_type);
            if (!strcmp(tmp_type, "none"))
            {
                p_param->type = TYPE_NONE;
            }
            else if (!strcmp(tmp_type, "file"))
            {
                p_param->type = TYPE_FILE;
            }
            else if (!strcmp(tmp_type, "vsphere"))
            {
                p_param->type = TYPE_VSPHERE;
            }
            else if (!strcmp(tmp_type, "fsphere"))
            {
                p_param->type = TYPE_FSPHERE;
            }
            else
            {
                fprintf(stderr, "%s: invalid type: %s\n", PROGRAM, tmp_type);
                return ERROR;
            }
            return DISCARD;
        }
        case KEY_FILE:
        {
            sscanf(arg, "-i%s", p_param->fileName);
            return DISCARD;
        }
        case KEY_MEMORY:
        {
            p_param->memoryFlag = true;
            return DISCARD;
        }
        case KEY_TMPDIR:
        {
            sscanf(arg, "-s%s", p_param->storeDir);
            return DISCARD;
        }
        case KEY_FILELIST:
        {
            sscanf(arg, "-p%s", p_param->backupFilelist);
            return DISCARD;
        }
        case KEY_DISKLIST:
        {
            sscanf(arg, "-l%s", p_param->backupDisklist);
            return DISCARD;
        }
        // 标识 非选项参数（即不以 - 或 -- 开头的参数）
        case FUSE_OPT_KEY_NONOPT:
        {
            ++p_param->strArgCount;
            switch (p_param->strArgCount)
            {
                case 1:
                {
                    // mountpoint
                    return KEEP;
                }
                default:
                {
                    fprintf(stderr, "%s: only one argument allowed: mountpoint\n", PROGRAM);
                    return ERROR;
                }
            }
        }
        default:
        {
            return KEEP;
        }
    }
}

/**
 * Check that we can write results to an archive file:
 * * file must be writable;
 * * parent directory must be writable (because the last step of archive saving
 *   is rename-and-replace).
 */
bool isFileWritable(const char *fileName)
{
    bool writable = true;
    if (access(fileName, F_OK) == 0 && access(fileName, W_OK) != 0)
    {
        // file exists but not writable
        writable = false;
    }
    else
    {
        char *fileNameDup = strdup(fileName);
        if (fileNameDup == NULL)
            throw std::bad_alloc();
        const char *dirName = dirname(fileNameDup);
        if (access(dirName, F_OK) == 0 && access(dirName, W_OK) != 0)
        {
            // parent directory is not writable
            writable = false;
        }
        free(fileNameDup);
    }
    return writable;
}

static const struct fuse_opt fusevm_opts[] = {
    FUSE_OPT_KEY("-h", KEY_HELP),
    FUSE_OPT_KEY("-v", KEY_VERSION),
    FUSE_OPT_KEY("-t %s", KEY_TYPE),
    FUSE_OPT_KEY("-i %s", KEY_FILE),
    FUSE_OPT_KEY("-m", KEY_MEMORY),
    FUSE_OPT_KEY("-s %s", KEY_TMPDIR),
    FUSE_OPT_KEY("-p %s", KEY_FILELIST),
    FUSE_OPT_KEY("-l %s", KEY_DISKLIST),
    {NULL, 0, 0}};

int main(int argc, char *argv[])
{
    if (sizeof(void *) > sizeof(uint64_t))
    {
        fprintf(stderr, "%s: This program cannot be run on your system because of FUSE design limitation\n", PROGRAM);
        return EXIT_FAILURE;
    }
    struct fuse_args args = FUSE_ARGS_INIT(argc, argv);
    FuseVMData *data = NULL;
    param.help = false;
    param.version = false;
    param.memoryFlag = false;
    param.strArgCount = 0;
    bzero(param.storeDir, sizeof(param.storeDir));
    bzero(param.backupFilelist, sizeof(param.backupFilelist));
    bzero(param.backupDisklist, sizeof(param.backupDisklist));
    bzero(param.fileName, sizeof(param.fileName));
    param.type = TYPE_NONE;
    if (fuse_opt_parse(&args, &param, fusevm_opts, process_arg))
    {
        fuse_opt_free_args(&args);
        return EXIT_FAILURE;
    }

    // if all work is done inside options parsing...
    if (param.help)
    {
        fuse_opt_free_args(&args);
        return EXIT_SUCCESS;
    }
    // pass version switch to HELP library to see it's version
    if (!param.version)
    {
        // no file name passed
        if(param.type == TYPE_NONE && strlen(param.fileName) != 0)
        {
            print_usage();
            fuse_opt_free_args(&args);
            return EXIT_FAILURE;
        }

        if (param.type != TYPE_NONE)
        {
            if(strlen(param.fileName) == 0 && (strlen(param.backupFilelist) == 0 || strlen(param.backupDisklist) == 0))
            {
                print_usage();
                fuse_opt_free_args(&args);
                return EXIT_FAILURE;
            }
        }
        if (strlen(param.storeDir) == 0)
        {
            param.memoryFlag = true;
        }
        else
        {
            param.memoryFlag = false;
        }
        /*
            if (!isFileWritable(param.fileName))
            {
                fuse_opt_free_args(&args);
                return EXIT_FAILURE;
            }
        */

        openlog(PROGRAM, LOG_PID, LOG_USER);
        if ((data = initFuseVM(PROGRAM)) == NULL)
        {
            fuse_opt_free_args(&args);
            return EXIT_FAILURE;
        }
    }

    static struct fuse_operations fusevm_oper;
    fusevm_oper.init = fusevm_init;
    fusevm_oper.destroy = fusevm_destroy;
    fusevm_oper.readdir = fusevm_readdir;
    fusevm_oper.getattr = fusevm_getattr;
    fusevm_oper.statfs = fusevm_statfs;
    fusevm_oper.open = fusevm_open;
    fusevm_oper.read = fusevm_read;
    fusevm_oper.write = fusevm_write;
    fusevm_oper.release = fusevm_release;
    fusevm_oper.unlink = fusevm_unlink;
    fusevm_oper.rmdir = fusevm_rmdir;
    fusevm_oper.mkdir = fusevm_mkdir;
    fusevm_oper.rename = fusevm_rename;
    fusevm_oper.create = fusevm_create;
    fusevm_oper.mknod = fusevm_mknod;
    fusevm_oper.chmod = fusevm_chmod;
    fusevm_oper.chown = fusevm_chown;
    fusevm_oper.flush = fusevm_flush;
    fusevm_oper.fsync = fusevm_fsync;
    fusevm_oper.fsyncdir = fusevm_fsyncdir;
    fusevm_oper.opendir = fusevm_opendir;
    fusevm_oper.releasedir = fusevm_releasedir;
    fusevm_oper.access = fusevm_access;
    fusevm_oper.utimens = fusevm_utimens;
    fusevm_oper.ftruncate = fusevm_ftruncate;
    fusevm_oper.truncate = fusevm_truncate;
    fusevm_oper.setxattr = fusevm_setxattr;
    fusevm_oper.getxattr = fusevm_getxattr;
    fusevm_oper.listxattr = fusevm_listxattr;
    fusevm_oper.removexattr = fusevm_removexattr;
    fusevm_oper.readlink = fusevm_readlink;
    fusevm_oper.symlink = fusevm_symlink;

#if FUSE_VERSION >= 28
    // don't allow NULL path
    fusevm_oper.flag_nullpath_ok = 0;
#if FUSE_VERSION == 29
    fusevm_oper.flag_utime_omit_ok = 1;
#endif
#endif

    struct fuse *fuse;
    char *mountpoint;
    // this flag ignored because vmfuse does not supports multithreading
    int multithreaded;
    int res;

    fuse = fuse_setup(args.argc, args.argv, &fusevm_oper, sizeof(fusevm_oper), &mountpoint, &multithreaded, data);
    fuse_opt_free_args(&args);
    if (fuse == NULL)
    {
        delete data;
        return EXIT_FAILURE;
    }
    res = fuse_loop(fuse);
    fuse_teardown(fuse, mountpoint);
    return (res == 0) ? EXIT_SUCCESS : EXIT_FAILURE;
}
